﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace ILLink.RoslynAnalyzer
{
    // Copied from https://github.com/dotnet/roslyn/blob/9c6d864baca08d7572871701ab583cec18279426/src/Compilers/Core/Portable/Operations/OperationExtensions.cs
    internal static partial class IOperationExtensions
    {
        /// <summary>
        /// Returns the <see cref="ValueUsageInfo"/> for the given operation.
        /// This extension can be removed once https://github.com/dotnet/roslyn/issues/25057 is implemented.
        /// </summary>
        public static ValueUsageInfo GetValueUsageInfo(this IOperation operation, ISymbol containingSymbol)
        {
            /*
            |    code                  | Read | Write | ReadableRef | WritableRef | NonReadWriteRef |
            | x.Prop = 1               |      |  ✔️   |             |             |                 |
            | x.Prop += 1              |  ✔️  |  ✔️   |             |             |                 |
            | x.Prop++                 |  ✔️  |  ✔️   |             |             |                 |
            | Foo(x.Prop)              |  ✔️  |       |             |             |                 |
            | Foo(x.Prop),             |      |       |     ✔️      |             |                 |
               where void Foo(in T v)
            | Foo(out x.Prop)          |      |       |             |     ✔️      |                 |
            | Foo(ref x.Prop)          |      |       |     ✔️      |     ✔️      |                 |
            | nameof(x)                |      |       |             |             |       ✔️        | ️
            | sizeof(x)                |      |       |             |             |       ✔️        | ️
            | typeof(x)                |      |       |             |             |       ✔️        | ️
            | out var x                |      |  ✔️   |             |             |                 | ️
            | case X x:                |      |  ✔️   |             |             |                 | ️
            | obj is X x               |      |  ✔️   |             |             |                 |
            | ref var x =              |      |       |     ✔️      |     ✔️      |                 |
            | ref readonly var x =     |      |       |     ✔️      |             |                 |
            */
            if (operation is ILocalReferenceOperation localReference &&
                localReference.IsDeclaration &&
                !localReference.IsImplicit) // Workaround for https://github.com/dotnet/roslyn/issues/30753
            {
                // Declaration expression is a definition (write) for the declared local.
                return ValueUsageInfo.Write;
            }
            else if (operation is IDeclarationPatternOperation)
            {
                while (operation.Parent is IBinaryPatternOperation ||
                       operation.Parent is INegatedPatternOperation ||
                       operation.Parent is IRelationalPatternOperation)
                {
                    operation = operation.Parent;
                }

                switch (operation.Parent)
                {
                    case IPatternCaseClauseOperation:
                        // A declaration pattern within a pattern case clause is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      switch (obj)
                        //      {
                        //          case X x:
                        //
                        return ValueUsageInfo.Write;

                    case IRecursivePatternOperation:
                        // A declaration pattern within a recursive pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      (obj) switch
                        //      {
                        //          (X x) => ...
                        //      };
                        //
                        return ValueUsageInfo.Write;

                    case ISwitchExpressionArmOperation:
                        // A declaration pattern within a switch expression arm is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      obj switch
                        //      {
                        //          X x => ...
                        //
                        return ValueUsageInfo.Write;

                    case IIsPatternOperation:
                        // A declaration pattern within an is pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj' below:
                        //      if (obj is X x)
                        //
                        return ValueUsageInfo.Write;

                    case IPropertySubpatternOperation:
                        // A declaration pattern within a property sub-pattern is a
                        // write for the declared local.
                        // For example, 'x' is defined and assigned the value from 'obj.Property' below:
                        //      if (obj is { Property : int x })
                        //
                        return ValueUsageInfo.Write;

                    default:
                        Debug.Fail("Unhandled declaration pattern context");

                        // Conservatively assume read/write.
                        return ValueUsageInfo.ReadWrite;
                }
            }

            if (operation.Parent is IAssignmentOperation assignmentOperation &&
                assignmentOperation.Target == operation)
            {
                return operation.Parent.IsAnyCompoundAssignment()
                    ? ValueUsageInfo.ReadWrite
                    : ValueUsageInfo.Write;
            }
            else if (operation.Parent is IIncrementOrDecrementOperation)
            {
                return ValueUsageInfo.ReadWrite;
            }
            else if (operation.Parent is IParenthesizedOperation parenthesizedOperation)
            {
                // Note: IParenthesizedOperation is specific to VB, where the parens cause a copy, so this cannot be classified as a write.
                Debug.Assert(parenthesizedOperation.Language == LanguageNames.VisualBasic);

                return parenthesizedOperation.GetValueUsageInfo(containingSymbol) &
                    ~(ValueUsageInfo.Write | ValueUsageInfo.Reference);
            }
            else if (operation.Parent is INameOfOperation ||
                       operation.Parent is ITypeOfOperation ||
                       operation.Parent is ISizeOfOperation)
            {
                return ValueUsageInfo.Name;
            }
            else if (operation.Parent is IArgumentOperation argumentOperation)
            {
                switch (argumentOperation.Parameter?.RefKind)
                {
                    case RefKind.RefReadOnly:
                        return ValueUsageInfo.ReadableReference;

                    case RefKind.Out:
                        return ValueUsageInfo.WritableReference;

                    case RefKind.Ref:
                        return ValueUsageInfo.ReadableWritableReference;

                    default:
                        return ValueUsageInfo.Read;
                }
            }
            else if (operation.Parent is IReturnOperation returnOperation)
            {
                return returnOperation.GetRefKind(containingSymbol) switch
                {
                    RefKind.RefReadOnly => ValueUsageInfo.ReadableReference,
                    RefKind.Ref => ValueUsageInfo.ReadableWritableReference,
                    _ => ValueUsageInfo.Read,
                };
            }
            else if (operation.Parent is IConditionalOperation conditionalOperation)
            {
                if (operation == conditionalOperation.WhenTrue
                    || operation == conditionalOperation.WhenFalse)
                {
                    return GetValueUsageInfo(conditionalOperation, containingSymbol);
                }
                else
                {
                    return ValueUsageInfo.Read;
                }
            }
            else if (operation.Parent is IReDimClauseOperation reDimClauseOperation &&
                  reDimClauseOperation.Operand == operation)
            {
                return (reDimClauseOperation.Parent as IReDimOperation)?.Preserve == true
                    ? ValueUsageInfo.ReadWrite
                    : ValueUsageInfo.Write;
            }
            else if (operation.Parent is IDeclarationExpressionOperation declarationExpression)
            {
                return declarationExpression.GetValueUsageInfo(containingSymbol);
            }
            else if (operation.IsInLeftOfDeconstructionAssignment(out _))
            {
                return ValueUsageInfo.Write;
            }
            else if (operation.Parent is IVariableInitializerOperation variableInitializerOperation)
            {
                if (variableInitializerOperation.Parent is IVariableDeclaratorOperation variableDeclaratorOperation)
                {
                    switch (variableDeclaratorOperation.Symbol.RefKind)
                    {
                        case RefKind.Ref:
                            return ValueUsageInfo.ReadableWritableReference;

                        case RefKind.RefReadOnly:
                            return ValueUsageInfo.ReadableReference;
                    }
                }
            }

            return ValueUsageInfo.Read;
        }

        public static RefKind GetRefKind(this IReturnOperation operation, ISymbol containingSymbol)
        {
            var containingMethod = TryGetContainingAnonymousFunctionOrLocalFunction(operation) ?? (containingSymbol as IMethodSymbol);
            return containingMethod?.RefKind ?? RefKind.None;
        }

        public static IMethodSymbol? TryGetContainingAnonymousFunctionOrLocalFunction(this IOperation? operation)
        {
            operation = operation?.Parent;
            while (operation != null)
            {
                switch (operation.Kind)
                {
                    case OperationKind.AnonymousFunction:
                        return ((IAnonymousFunctionOperation)operation).Symbol;

                    case OperationKind.LocalFunction:
                        return ((ILocalFunctionOperation)operation).Symbol;
                }

                operation = operation.Parent;
            }

            return null;
        }

        public static bool IsInLeftOfDeconstructionAssignment(this IOperation operation, out IDeconstructionAssignmentOperation? deconstructionAssignment)
        {
            deconstructionAssignment = null;

            var previousOperation = operation;
            var current = operation.Parent;

            while (current != null)
            {
                switch (current.Kind)
                {
                    case OperationKind.DeconstructionAssignment:
                        deconstructionAssignment = (IDeconstructionAssignmentOperation)current;
                        return deconstructionAssignment.Target == previousOperation;

                    case OperationKind.Tuple:
                    case OperationKind.Conversion:
                    case OperationKind.Parenthesized:
                        previousOperation = current;
                        current = current.Parent;
                        continue;

                    default:
                        return false;
                }
            }

            return false;
        }

        /// <summary>
        /// Retursn true if the given operation is a regular compound assignment,
        /// i.e. <see cref="ICompoundAssignmentOperation"/> such as <code>a += b</code>,
        /// or a special null coalescing compoud assignment, i.e. <see cref="ICoalesceAssignmentOperation"/>
        /// such as <code>a ??= b</code>.
        /// </summary>
        public static bool IsAnyCompoundAssignment(this IOperation operation)
        {
            switch (operation)
            {
                case ICompoundAssignmentOperation:
                case ICoalesceAssignmentOperation:
                    return true;

                default:
                    return false;
            }
        }

        /// <summary>
        /// Finds the symbol of the caller to the current operation, helps to find out the symbol in cases where the operation passes
        /// through a lambda or a local function.
        /// </summary>
        /// <param name="operation">The operation to find the symbol for.</param>
        /// <param name="owningSymbol">The owning symbol of the entire operation context.</param>
        /// <returns>The symbol of the caller to the operation</returns>
        public static ISymbol FindContainingSymbol(this IOperation operation, ISymbol owningSymbol)
        {
            var parent = operation.Parent;
            while (parent is not null)
            {
                switch (parent)
                {
                    case IAnonymousFunctionOperation lambda:
                        return lambda.Symbol;

                    case ILocalFunctionOperation local:
                        return local.Symbol;

                    case IMethodBodyBaseOperation:
                    case IPropertyReferenceOperation:
                    case IFieldReferenceOperation:
                    case IEventReferenceOperation:
                        return owningSymbol;

                    default:
                        parent = parent.Parent;
                        break;
                }
            }

            return owningSymbol;
        }
    }
}
